﻿using System;
using System.IO;
using System.IO.Ports;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Text;

namespace LogicProtocolAnalyzer
{
    enum SampleFrequency
    {
        _2MHz = 0,
        _200kHz = 1,
        _20kHz = 2,
        _2kHz = 3,
        Unknown
    }

    enum TriggerMode
    {
        AutoCmp = 0,
        AutoEdge = 1,
        Manual = 2,
        Unknown
    }

    enum TriggerEdge
    {
        Falling = 0,
        Rising = 1,
        Unknown
    }

    internal enum Level
    {
        High,
        Low
    }

    internal static class ExtensionMethods
    {
        public const string Unknown = "Unknown";

        internal static string AsString(this SampleFrequency sampleFrequency)
        {
            switch(sampleFrequency)
            {
                case SampleFrequency._2kHz: return "2kHz";
                case SampleFrequency._20kHz: return "20kHz";
                case SampleFrequency._200kHz: return "200kHz";
                case SampleFrequency._2MHz: return "2MHz";
            }
            return Unknown;
        }

        internal static float AsFrequency(this SampleFrequency sampleFrequency)
        {
            switch (sampleFrequency)
            {
                case SampleFrequency._2kHz: return 2000;
                case SampleFrequency._20kHz: return 20000;
                case SampleFrequency._200kHz: return 200000;
                case SampleFrequency._2MHz: return 2000000;
            }
            return 0;
        }

        internal static string AsString(this TriggerMode triggerMode)
        {
            switch(triggerMode)
            {
                case TriggerMode.AutoCmp: return "Auto CMP";
                case TriggerMode.AutoEdge: return "Auto EDGE";
                case TriggerMode.Manual: return "Manual";
            }
            return Unknown;
        }

        internal static string PrettyPrintTime(this double time)
        {
            if (time < (1.0 / 1000.0))
            {
                return string.Format("{0} us", time * 1000000); // Display in micro seconds
            }
            else if (time < 1.0)
            {
                return string.Format("{0} ms", time * 1000); // Display in milli seconds
            }
            return string.Format("{0} s", time); // Display in seconds
        }
    }

    // File format:
    //
    // 13 bytes header
    // n * 256 bytes samples
    // 1 byte tail (soort checksum??)
    //
    // Header format:
    //    0  -  Sample size in 256 Byte blocks
    //    1  -  00
    //    2  -  Sample frequency: 0 = 2 MHz, 1= 200 kHz, 2 = 20 kHz, 3 = 2 kHz
    //    3  -  Trigger mode: 0 = autocmp, 1 = autoedge, 2 = manual
    //    4  -  ch0: autocmp 0 = L, 1 = H; autoedge 0 = falling, 1 = rising
    //    5  -  ch1: autocmp 0 = L, 1 = H; autoedge 0 = falling, 1 = rising
    //    6  -  ch2: autocmp 0 = L, 1 = H; autoedge 0 = falling, 1 = rising
    //    7  -  ch3: autocmp 0 = L, 1 = H; autoedge 0 = falling, 1 = rising
    //    8  -  ch4: autocmp 0 = L, 1 = H; autoedge 0 = falling, 1 = rising
    //    9  -  ch5: autocmp 0 = L, 1 = H; autoedge 0 = falling, 1 = rising
    //    10 -  ch6: autocmp 0 = L, 1 = H; autoedge 0 = falling, 1 = rising
    //    11 -  ch7: autocmp 0 = L, 1 = H; autoedge 0 = falling, 1 = rising
    //    12 -  autoedge channel: 0 = 0, ... , 7 = 7

    internal class SampledData
    {
        /// <summary>
        /// Polynomial table x^8 + x^5 + x^4 + 1 - used in Dallas
        /// Semiconductor iButton devices (1-wire)
        /// </summary>
        private static readonly byte[] PolynomialTable =
        {
            0x00, 0x5e, 0xbc, 0xe2, 0x61, 0x3f, 0xdd, 0x83, 
            0xc2, 0x9c, 0x7e, 0x20, 0xa3, 0xfd, 0x1f, 0x41, 
            0x9d, 0xc3, 0x21, 0x7f, 0xfc, 0xa2, 0x40, 0x1e, 
            0x5f, 0x01, 0xe3, 0xbd, 0x3e, 0x60, 0x82, 0xdc, 
            0x23, 0x7d, 0x9f, 0xc1, 0x42, 0x1c, 0xfe, 0xa0, 
            0xe1, 0xbf, 0x5d, 0x03, 0x80, 0xde, 0x3c, 0x62, 
            0xbe, 0xe0, 0x02, 0x5c, 0xdf, 0x81, 0x63, 0x3d, 
            0x7c, 0x22, 0xc0, 0x9e, 0x1d, 0x43, 0xa1, 0xff, 
            0x46, 0x18, 0xfa, 0xa4, 0x27, 0x79, 0x9b, 0xc5, 
            0x84, 0xda, 0x38, 0x66, 0xe5, 0xbb, 0x59, 0x07, 
            0xdb, 0x85, 0x67, 0x39, 0xba, 0xe4, 0x06, 0x58, 
            0x19, 0x47, 0xa5, 0xfb, 0x78, 0x26, 0xc4, 0x9a, 
            0x65, 0x3b, 0xd9, 0x87, 0x04, 0x5a, 0xb8, 0xe6, 
            0xa7, 0xf9, 0x1b, 0x45, 0xc6, 0x98, 0x7a, 0x24, 
            0xf8, 0xa6, 0x44, 0x1a, 0x99, 0xc7, 0x25, 0x7b, 
            0x3a, 0x64, 0x86, 0xd8, 0x5b, 0x05, 0xe7, 0xb9, 
            0x8c, 0xd2, 0x30, 0x6e, 0xed, 0xb3, 0x51, 0x0f, 
            0x4e, 0x10, 0xf2, 0xac, 0x2f, 0x71, 0x93, 0xcd, 
            0x11, 0x4f, 0xad, 0xf3, 0x70, 0x2e, 0xcc, 0x92, 
            0xd3, 0x8d, 0x6f, 0x31, 0xb2, 0xec, 0x0e, 0x50, 
            0xaf, 0xf1, 0x13, 0x4d, 0xce, 0x90, 0x72, 0x2c, 
            0x6d, 0x33, 0xd1, 0x8f, 0x0c, 0x52, 0xb0, 0xee, 
            0x32, 0x6c, 0x8e, 0xd0, 0x53, 0x0d, 0xef, 0xb1, 
            0xf0, 0xae, 0x4c, 0x12, 0x91, 0xcf, 0x2d, 0x73, 
            0xca, 0x94, 0x76, 0x28, 0xab, 0xf5, 0x17, 0x49, 
            0x08, 0x56, 0xb4, 0xea, 0x69, 0x37, 0xd5, 0x8b, 
            0x57, 0x09, 0xeb, 0xb5, 0x36, 0x68, 0x8a, 0xd4, 
            0x95, 0xcb, 0x29, 0x77, 0xf4, 0xaa, 0x48, 0x16, 
            0xe9, 0xb7, 0x55, 0x0b, 0x88, 0xd6, 0x34, 0x6a, 
            0x2b, 0x75, 0x97, 0xc9, 0x4a, 0x14, 0xf6, 0xa8, 
            0x74, 0x2a, 0xc8, 0x96, 0x15, 0x4b, 0xa9, 0xf7, 
            0xb6, 0xe8, 0x0a, 0x54, 0xd7, 0x89, 0x6b, 0x35,
        };

        /// <summary>
        /// number of sample pages (van 256 bytes) (1 = 1024, 2 = 2048, 3 = 4096, 4 = 8192, 5 = 16384, 6 = 32768)
        /// </summary>
        private byte[] _data;
        private Settings _fileSettings;

        internal SampleFrequency SampleFrequency { get { return _fileSettings.SampleFrequency; } }
        internal byte Checksum { get; private set; }
        internal string Settings { get { return string.Format("{0}, {1} samples, {2}", _fileSettings.SampleFrequency.AsString(), _fileSettings.SampleSize * 256, _fileSettings.TriggerMode.AsString()); } }
        internal byte this[int index] { get { return _data[index]; } }
        internal int Length
        {
            get
            {
                if(_data == null) return 0;
                return _data.Length; 
            }
        }

        internal SampledData()
        {
            _fileSettings = new Settings();
        }

        /// <summary>
        /// To faillitate the use the foreach to get the content of the _data buffer
        /// </summary>
        /// <returns></returns>
        public IEnumerator GetEnumerator()
        {
            int dataSize = _data.Length;
            for (int index = 13; index < dataSize - 1; index++)
            {
                yield return _data[index];
            }
        }

        internal void Load(Stream stream)
        {
            byte[] data = new byte[stream.Length];
            stream.Read(data, 0, (int)stream.Length);

            // TODO add checks

            _fileSettings.SampleSize = data[0];
            // second byte should be 0
            _fileSettings.SampleFrequency = (Enum.IsDefined(typeof(SampleFrequency), (SampleFrequency)data[2])) ? (SampleFrequency)data[2] : SampleFrequency.Unknown;
            _fileSettings.TriggerMode = (Enum.IsDefined(typeof(TriggerMode), (TriggerMode)data[3])) ? (TriggerMode)data[3] : TriggerMode.Unknown;
            for (int channel = 0; channel < AnalyzerControl.NumberOfChannels; channel++)
            {
                _fileSettings.SetAutoCompareTriggerLevel(channel, data[channel+4] == 0 ? Level.Low : Level.High);
            }
            _fileSettings.AutoEdgeChannelIndex = data[12];
            this.Checksum = data[data.Length - 1];

            const int HeaderLength = 13;
            _data = new byte[data.Length - (HeaderLength + 1)];
            Array.Copy(data, HeaderLength/*skip header at start*/, _data, 0, data.Length - (HeaderLength + 1/* skip checksum at end */));
        }

        internal void Save(Stream stream)
        {
        }

        /// <summary>
        /// Count 8-bit CRC
        /// Polynomial: x^8+x^5+x^4+1
        /// (same as in Dallas' iButtons)
        /// </summary>
        private static byte Crc8Dallas(List<byte> data)
        {
            byte crc = 0;
            foreach (byte value in data)
            {
                crc = PolynomialTable[value ^ crc];
            }
            return crc;
        }

        internal void Acquire(BackgroundWorker backgroundWorker, DoWorkEventArgs doWorkEventArgs)
        {
            _fileSettings = doWorkEventArgs.Argument as Settings;

            byte[] commsBuffer = new byte[20];

            // instruct analyser to gather data, wait for it to complete & retrieve data.

            // Instantiate port to analyser
            using (SerialPort serialPort = new SerialPort(_fileSettings.PortName, 38400, Parity.None, 8, StopBits.One))
            {
                serialPort.ReadTimeout = 100;
                // Open the port for communications
                serialPort.Open();

                // Send wake up string
                serialPort.Write("Q");  // Send reset; no reply from analyzer
                System.Threading.Thread.Sleep(30);
                serialPort.Write("I");  // Wakeup; expected response: 'SLA_ANTONIAK_V1'

                string expectedResponse = "SLA_ANTONIAK_V1";
                StringBuilder receivedResponse = new StringBuilder(expectedResponse.Length);
                for (int index = 0; index < expectedResponse.Length; index++)
                {
                    receivedResponse.Append((char)serialPort.ReadChar());
                }

                if (receivedResponse.ToString() != expectedResponse)
                {
                    throw new UnexpectedResponseException(string.Format("The analyzer responded with '{0}' while '{1}' was excpected.", receivedResponse.ToString(), expectedResponse));
                }

                // Create & Send Query-frame which sets the sample parameters and starts aquisition.
                commsBuffer[0] = 65;                                        // todo ** = check & correct domain, * = verify it works okay
                switch (_fileSettings.TriggerMode)                                                           // trigger mode
                {
                    case TriggerMode.Manual:    // tested, okay.
                        commsBuffer[1] = 1;
                        commsBuffer[2] = 0;
                        commsBuffer[3] = 0;
                        break;
                    case TriggerMode.AutoCmp:   // tested, okay.
                        commsBuffer[1] = 2;
                        commsBuffer[2] = 0;
                        commsBuffer[3] = 0;
                        commsBuffer[3] |= (byte)((_fileSettings.GetAutoCompareTriggerLevel(0) == Level.High) ? 1 : 0);
                        commsBuffer[3] |= (byte)((_fileSettings.GetAutoCompareTriggerLevel(1) == Level.High) ? 2 : 0);
                        commsBuffer[3] |= (byte)((_fileSettings.GetAutoCompareTriggerLevel(2) == Level.High) ? 4 : 0);
                        commsBuffer[3] |= (byte)((_fileSettings.GetAutoCompareTriggerLevel(3) == Level.High) ? 8 : 0);
                        commsBuffer[3] |= (byte)((_fileSettings.GetAutoCompareTriggerLevel(4) == Level.High) ? 16 : 0);
                        commsBuffer[3] |= (byte)((_fileSettings.GetAutoCompareTriggerLevel(5) == Level.High) ? 32 : 0);
                        commsBuffer[3] |= (byte)((_fileSettings.GetAutoCompareTriggerLevel(6) == Level.High) ? 64 : 0);
                        commsBuffer[3] |= (byte)((_fileSettings.GetAutoCompareTriggerLevel(7) == Level.High) ? 128 : 0);
                        break;
                    case TriggerMode.AutoEdge:  
                        commsBuffer[1] = 3;
                        commsBuffer[2] = (byte)_fileSettings.AutoEdgeChannelIndex;                                   // ** edge pin
                        commsBuffer[3] = (byte)((_fileSettings.AutoEdgeDirection == TriggerEdge.Falling) ? 1 : 0);   // * rising/falling edge
                        break;
                }
                commsBuffer[4] = (byte)(_fileSettings.SampleFrequency + 1);                                  // tested, okay
                commsBuffer[5] = (byte)(_fileSettings.SampleSize + 1);                                       // tested, okay.
                commsBuffer[6] = (byte)(commsBuffer[1] + commsBuffer[2] + commsBuffer[3] + commsBuffer[4] + commsBuffer[5]);    // sum
                serialPort.Write(commsBuffer, 0, 7);

                Console.WriteLine(string.Format("Sampling params: {0} {1} {2} {3} {4}",
                    commsBuffer[1], commsBuffer[2], commsBuffer[3], commsBuffer[4],commsBuffer[5]));

                System.Threading.Thread.Sleep(10);
                Console.WriteLine("Wait for sampling to complete");
                System.Threading.Thread.Sleep(100);


                // now we wait for the sampling to end
                while (serialPort.BytesToRead == 0)
                {
                    if (backgroundWorker.CancellationPending)
                    {
                        doWorkEventArgs.Cancel = true;
                        return;
                    }

                    System.Threading.Thread.Sleep(500);
                }

                int block = 0;   // we get the first (1k) block automaticly when sampling is complete
                int nrBlocks = 1;   // nr of 1k blocks we expect
                switch (_fileSettings.SampleSize)
                {
                    case 0: nrBlocks = 1; break;
                    case 1: nrBlocks = 2; break;
                    case 2: nrBlocks = 4; break;
                    case 3: nrBlocks = 8; break;
                    case 4: nrBlocks = 16; break;
                    case 5: nrBlocks = 32; break;
                }

                Console.WriteLine(string.Format("receive block {0}", block));
                List<byte> analyserData = new List<byte>();
                for (; ; )
                {
                    if (backgroundWorker.CancellationPending)
                    {
                        doWorkEventArgs.Cancel = true;
                        return;
                    }


                    int count = 50;
                    // Wait until 1024 are received
                    while (serialPort.BytesToRead < 1024)
                    {
                        int percentProgress = 100 * ((block * 1024) + serialPort.BytesToRead) / (nrBlocks * 1024);
                        backgroundWorker.ReportProgress(percentProgress);

                        // wait up to 50 * 10  ms for the page to receive
                        System.Threading.Thread.Sleep(10);
                        count--;
                        if (count == 0)
                        {
                            throw new TimeoutException(string.Format("comms timeout 3, block: {0}, bytes: {1}", block, serialPort.BytesToRead));
                        }
                    }
                    // read all 1024 bytes (one block)
                    for (int i = 0; i < 1024; i++)
                    {
                        analyserData.Add((byte)serialPort.ReadByte());
                    }
                    block++;
                    if (block < nrBlocks)
                    {
                        Console.WriteLine(string.Format("receive block {0}", block));
                        commsBuffer[0] = (byte)block;
                        serialPort.Write(commsBuffer, 0, 1);
                    }
                    else
                    {
                        break;
                    }
                }
                Console.WriteLine(string.Format("received all {0} bytes", analyserData.Count));

                // get checksum and reset analyser
                Console.WriteLine(string.Format("bytetoread @1 {0}", serialPort.BytesToRead));
                
                serialPort.Write("C");

                byte receivedCrc = (byte)serialPort.ReadByte();

                byte calculatedCrc = Crc8Dallas(analyserData);

                if (receivedCrc != calculatedCrc)
                {
                    throw new CrcException(string.Format("!!! CRC Error received: {0}, calculated: {1}", receivedCrc, calculatedCrc));
                }
                Console.WriteLine("CRC Okay.");

                // Close the port
                serialPort.Close();

                System.Threading.Thread.Sleep(100);
                Console.WriteLine("communications done");

                _data = analyserData.ToArray();
            }
        }
    }
}
